/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.dataconn.jdbc.pool.interceptor;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.apache.log4j.Logger;
import org.lora.log4j.Log4jUtil;

/**
 * Implementation of <b>JdbcInterceptor</b> that proxies resultSets and
 * statements.
 * 
 * @author Guillermo Fernandes
 */
public class StatementDecoratorInterceptor extends AbstractCreateStatementInterceptor {

	private static final Logger logger = Log4jUtil.getSystemLogger();

	protected static final String EXECUTE_QUERY = "executeQuery";
	protected static final String GET_GENERATED_KEYS = "getGeneratedKeys";
	protected static final String GET_RESULTSET = "getResultSet";

	protected static final String[] RESULTSET_TYPES = { EXECUTE_QUERY, GET_GENERATED_KEYS, GET_RESULTSET };

	/**
	 * the constructors that are used to create statement proxies
	 */
	protected static final Constructor<?>[] constructors = new Constructor[AbstractCreateStatementInterceptor.STATEMENT_TYPE_COUNT];

	/**
	 * the constructor to create the resultSet proxies
	 */
	protected static Constructor<?> resultSetConstructor = null;

	@Override
	public void closeInvoked() {
		// nothing to do
	}

	/**
	 * Creates a constructor for a proxy class, if one doesn't already exist
	 *
	 * @param idx
	 *            - the index of the constructor
	 * @param clazz
	 *            - the interface that the proxy will implement
	 * @return - returns a constructor used to create new instances
	 * @throws NoSuchMethodException
	 */
	protected Constructor<?> getConstructor(int idx, Class<?> clazz) throws NoSuchMethodException {
		if (constructors[idx] == null) {
			Class<?> proxyClass = Proxy.getProxyClass(StatementDecoratorInterceptor.class.getClassLoader(), new Class[] { clazz });
			constructors[idx] = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
		}
		return constructors[idx];
	}

	protected Constructor<?> getResultSetConstructor() throws NoSuchMethodException {
		if (resultSetConstructor == null) {
			Class<?> proxyClass = Proxy
					.getProxyClass(StatementDecoratorInterceptor.class.getClassLoader(), new Class[] { ResultSet.class });
			resultSetConstructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
		}
		return resultSetConstructor;
	}

	/**
	 * Creates a statement interceptor to monitor query response times
	 */
	@Override
	public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
		try {
			String name = method.getName();
			Constructor<?> constructor = null;
			String sql = null;
			if (compare(CREATE_STATEMENT, name)) {
				// createStatement
				constructor = getConstructor(CREATE_STATEMENT_IDX, Statement.class);
			} else if (compare(PREPARE_STATEMENT, name)) {
				// prepareStatement
				constructor = getConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class);
				sql = (String) args[0];
			} else if (compare(PREPARE_CALL, name)) {
				// prepareCall
				constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class);
				sql = (String) args[0];
			} else {
				// do nothing, might be a future unsupported method
				// so we better bail out and let the system continue
				return statement;
			}
			return createDecorator(proxy, method, args, statement, constructor, sql);
		} catch (Exception x) {
			if (x instanceof InvocationTargetException) {
				Throwable cause = x.getCause();
				if (cause instanceof ThreadDeath) {
					throw (ThreadDeath) cause;
				}
				if (cause instanceof VirtualMachineError) {
					throw (VirtualMachineError) cause;
				}
			}
			logger.error("Unable to create statement proxy for slow query report.", x);
		}
		return statement;
	}

	/**
	 * Creates a proxy for a Statement.
	 *
	 * @param proxy
	 *            The proxy object on which the method that triggered the
	 *            creation of the statement was called.
	 * @param method
	 *            The method that was called on the proxy
	 * @param args
	 *            The arguments passed as part of the method call to the proxy
	 * @param statement
	 *            The statement object that is to be proxied
	 * @param constructor
	 *            The constructor for the desired proxy
	 * @param sql
	 *            The sql of of the statement
	 *
	 * @return A new proxy for the Statement
	 */
	protected Object createDecorator(Object proxy, Method method, Object[] args, Object statement, Constructor<?> constructor, String sql)
			throws InstantiationException, IllegalAccessException, InvocationTargetException {
		Object result = null;
		StatementProxy<Statement> statementProxy = new StatementProxy<Statement>((Statement) statement, sql);
		result = constructor.newInstance(new Object[] { statementProxy });
		statementProxy.setActualProxy(result);
		statementProxy.setConnection(proxy);
		statementProxy.setConstructor(constructor);
		return result;
	}

	protected boolean isExecuteQuery(String methodName) {
		return EXECUTE_QUERY.equals(methodName);
	}

	protected boolean isExecuteQuery(Method method) {
		return isExecuteQuery(method.getName());
	}

	protected boolean isResultSet(Method method, boolean process) {
		return process(RESULTSET_TYPES, method, process);
	}

	/**
	 * Class to measure query execute time
	 *
	 * @author fhanik
	 *
	 */
	protected class StatementProxy<T extends java.sql.Statement> implements InvocationHandler {

		protected boolean closed = false;
		protected T delegate;
		private Object actualProxy;
		private Object connection;
		private String sql;
		private Constructor<?> constructor;

		public StatementProxy(T delegate, String sql) {
			this.delegate = delegate;
			this.sql = sql;
		}

		public T getDelegate() {
			return this.delegate;
		}

		public String getSql() {
			return sql;
		}

		public void setConnection(Object proxy) {
			this.connection = proxy;
		}

		public Object getConnection() {
			return this.connection;
		}

		public void setActualProxy(Object proxy) {
			this.actualProxy = proxy;
		}

		public Object getActualProxy() {
			return this.actualProxy;
		}

		public Constructor<?> getConstructor() {
			return constructor;
		}

		public void setConstructor(Constructor<?> constructor) {
			this.constructor = constructor;
		}

		public void closeInvoked() {
			if (getDelegate() != null) {
				try {
					getDelegate().close();
				} catch (SQLException ignore) {
				}
			}
			closed = true;
			delegate = null;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			if (compare(TOSTRING_VAL, method)) {
				return toString();
			}
			// was close invoked?
			boolean close = compare(CLOSE_VAL, method);
			// allow close to be called multiple times
			if (close && closed)
				return null;
			// are we calling isClosed?
			if (compare(ISCLOSED_VAL, method))
				return Boolean.valueOf(closed);
			// if we are calling anything else, bail out
			if (closed)
				throw new SQLException("Statement closed.");
			if (compare(GETCONNECTION_VAL, method)) {
				return connection;
			}
			boolean process = false;
			process = isResultSet(method, process);
			// check to see if we are about to execute a query
			// if we are executing, get the current time
			Object result = null;
			try {
				// perform close cleanup
				if (close) {
					closeInvoked();
				} else {
					// execute the query
					result = method.invoke(delegate, args);
				}
			} catch (Throwable t) {
				if (t instanceof InvocationTargetException && t.getCause() != null) {
					throw t.getCause();
				} else {
					throw t;
				}
			}
			if (process && result != null) {
				Constructor<?> cons = getResultSetConstructor();
				result = cons.newInstance(new Object[] { new ResultSetProxy(actualProxy, result) });
			}
			return result;
		}

		@Override
		public String toString() {
			StringBuffer buf = new StringBuffer(StatementProxy.class.getName());
			buf.append("[Proxy=");
			buf.append(System.identityHashCode(this));
			buf.append("; Sql=");
			buf.append(getSql());
			buf.append("; Delegate=");
			buf.append(getDelegate());
			buf.append("; Connection=");
			buf.append(getConnection());
			buf.append("]");
			return buf.toString();
		}
	}

	protected class ResultSetProxy implements InvocationHandler {

		private Object st;
		private Object delegate;

		public ResultSetProxy(Object st, Object delegate) {
			this.st = st;
			this.delegate = delegate;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			if (method.getName().equals("getStatement")) {
				return this.st;
			} else {
				try {
					return method.invoke(this.delegate, args);
				} catch (Throwable t) {
					if (t instanceof InvocationTargetException && t.getCause() != null) {
						throw t.getCause();
					} else {
						throw t;
					}
				}
			}
		}
	}
}
